package com.perceptivesoftware.security;

import java.util.ArrayList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.stereotype.Component;
import com.perceptivesoftware.licensing.server.handler.PropertiesHandler;
import com.perceptivesoftware.licensing.server.util.ServerUtils;

@Component
public class PerceptiveAuthenticationProviderWrapper implements AuthenticationProvider {

	private final Log logger = LogFactory.getLog(getClass());

	private AuthenticationProvider authProvider;

	@Autowired
	@Qualifier("casEntryPoint")
	CasAuthenticationEntryPoint casEntryPoint;

	private Boolean isCasConfigFile = null;

	private Boolean configured = false;

	final PropertiesHandler propertiesHandler = new PropertiesHandler();

	// Called once isCasConfigFile is set instead of PostConstruct to get around Spring instantiation issue.
	public void configure() {
		configured = true;

		final AuthType authType = AuthType.fromString(propertiesHandler.getProperty("auth.type"));
		logger.info("Authentication Detected: " + authType.toString());
		verifyCas(authType);

		switch (authType) {
		case DB:
			handleDbAuth();
			return;
		case CAS:
			handleCASAuth();
			return;
		case LDAP:
			handleLDAPAuth();
			return;
		case CUSTOM:
			authProvider = new PerceptiveDbAuthenticationProvider();
			return;
		}
		throw new RuntimeException("Unknown AuthType specified");
	}

	private void handleDbAuth() {
		authProvider = new PerceptiveDbAuthenticationProvider();
	}

	private void handleLDAPAuth() {
		final DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(property("auth.LdapConnString"));
		contextSource.setUserDn(property("auth.LdapBrowseUserDn"));
		contextSource.setPassword(property("auth.LdapBrowsePassword"));
		try {
			contextSource.afterPropertiesSet();
		}
		catch (final Exception e) {
			throw new RuntimeException("Error initializing Ldap", e);
		}
		final String multipleOus = property("auth.LdapMultipleOus", "");
		final String memberOf = property("auth.LdapMemberOf", "");
		String filter = property("auth.LdapFilter", "sAMAccountName={0}");
		final BindAuthenticator bindAuth = new BindAuthenticator(contextSource);
		if (multipleOus.length() > 0) {
			if (memberOf.length() > 0) {
				throw new RuntimeException("Cannot specify LdapMultipleOus AND LdapMemberOf");
			}
			final ArrayList<String> userDnPatterns = new ArrayList<String>();
			for (final String dn : multipleOus.split(ServerUtils.semiColonRegex)) {
				userDnPatterns.add(filter + "," + dn);
			}
			bindAuth.setUserDnPatterns(userDnPatterns.toArray(new String[] {}));
		}
		else {
			if (memberOf.length() > 0) {
				filter = "(&(" + filter + ")(memberOf=" + memberOf + "))";
			}
			final FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(property("auth.LdapOu"), filter, contextSource);
			bindAuth.setUserSearch(userSearch);
		}

		final LdapAuthenticationProvider ldapAuthProvider = new LdapAuthenticationProvider(bindAuth, new PerceptiveLdapAuthoritiesPopulator());

		authProvider = ldapAuthProvider;
	}

	private void handleCASAuth() {
		/*
		 * 
		 * final org.springframework.security.cas.ServiceProperties serviceProperties = new org.springframework.security.cas.ServiceProperties();
		 * serviceProperties.setService(property("auth.CasNtUrl") + "j_spring_cas_security_check");
		 * serviceProperties.setSendRenew(Boolean.getBoolean(property("auth.CasSendRenew", "false"))); casEntryPoint.setLoginUrl(property("auth.CasLoginUrl"));
		 * casEntryPoint.setServiceProperties(serviceProperties); final Cas20ServiceTicketValidator ticketValidator = new
		 * Cas20ServiceTicketValidator(property("auth.CasServerUrl")); final PerceptiveCasUserService userService = new PerceptiveCasUserService(); final
		 * CasAuthenticationProvider casAuthProvider = new CasAuthenticationProvider(); casAuthProvider.setUserDetailsService(userService);
		 * casAuthProvider.setServiceProperties(serviceProperties); casAuthProvider.setTicketValidator(ticketValidator);
		 * casAuthProvider.setKey("request_handler_authentication_provider"); authProvider = casAuthProvider;
		 */
	}

	private void verifyCas(AuthType authType) {
		if (authType == AuthType.CAS && !isCasConfigFile) {
			throw new RuntimeException(
					"ERROR: CAS specified witout using CAS config file. Delete perceptivelicensing-security.xml and rename perceptivelicensing-security.xml.cas to perceptivelicensing-security.xml");
		}
		if (authType != AuthType.CAS && isCasConfigFile) {
			throw new RuntimeException("CAS config fule used without specifying CAS authentication.");
		}
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		return authProvider.authenticate(authentication);
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return authProvider.supports(authentication);
	}

	public void setIsCasConfigFile(Boolean isCasConfigFile) {
		this.isCasConfigFile = isCasConfigFile;
		if (!configured) {
			configure();
		}
	}

	enum AuthType {
		DB, LDAP, CAS, WEBAUTH, UW, CUSTOM;

		public static AuthType fromString(String property) {
			if (property == null) {
				return DB;
			}
			return AuthType.valueOf(property);
		}
	}

	private String property(String string) {
		return ServerUtils.property(string);
	}

	private String property(String propKey, String defaultVal) {
		return ServerUtils.property(propKey, defaultVal);
	}
}
